package jmine.tec.subscription.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import jmine.tec.subscription.Publication;
import jmine.tec.subscription.PublicationListener;
import jmine.tec.subscription.Publisher;
import jmine.tec.subscription.Subscription;
import jmine.tec.subscription.SubscriptionSet;
import jmine.tec.subscription.SubscriptionSettings;

/**
 * Implementacao trivial de {@link SubscriptionSet}. Os principais metodos sao sincronizados pois, em teoria, varias threads nao podem /
 * devem acessar uma mesma {@link SubscriptionSet} ao mesmo tempo. Um {@link SubscriptionSetImpl} pode guardar publicacoes para clientes
 * 'remotos' - clientes que fazem poll de mudancas ao inves de push. Alem disso, a {@link SubscriptionSetImpl} guarda apenas a ultima
 * {@link Publication} para cada {@link Subscription}
 * 
 * @author takeshi
 */
public class SubscriptionSetImpl implements SubscriptionSet {
    private long version;

    // @GuardedBy this
    private final Map<Subscription, Publication> remainingPublications = new HashMap<Subscription, Publication>();

    // @GuardedBy this
    private final Map<Subscription, PublicationListener<?>> managedSubscriptions = new HashMap<Subscription, PublicationListener<?>>();

    // @GuardedBy this
    private final Map<Long, Subscription> tokens = new HashMap<Long, Subscription>();

    private long tokenGenerator;

    /**
     * {@link SubscriptionSetImpl} com timeout default
     */
    public SubscriptionSetImpl() {
    }

    /**
     * {@inheritDoc}
     */
    public long nextTimeout() {
        return Long.MAX_VALUE;
    }

    /**
     * {@inheritDoc}
     */
    public long getToken() {
        return System.identityHashCode(this);
    }

    /**
     * @return the version
     */
    public long getVersion() {
        return this.version;
    }

    /**
     * @param version the version to set
     */
    public void setVersion(long version) {
        this.version = version;
    }

    /**
     * {@inheritDoc}
     */
    public void renewTimeout() {
    }

    /**
     * Este {@link SubscriptionSet} nunca expira
     * 
     * @return <code>false</code>
     */
    public boolean isExpired() {
        return false;
    }

    /**
     * "empurra" uma publicacao
     * 
     * @param publication {@link Publication}
     * @param s {@link Subscription}
     */
    public synchronized void publish(Publication publication, Subscription s) {
        if (this.managedSubscriptions.containsKey(s)) {
            this.remainingPublications.put(s, publication);
            s.incrementVersion();
        }
    }

    /**
     * @return {@link Map}
     */
    public synchronized Map<Subscription, Publication> getPublications() {
        final HashMap<Subscription, Publication> hashMap = new HashMap<Subscription, Publication>(this.remainingPublications);
        this.remainingPublications.clear();
        return hashMap;
    }

    /**
     * Adiciona uma {@link Subscription}
     * 
     * @param s a {@link Subscription} a ser adicionada
     * @param listener {@link PublicationListener} associado
     */
    public synchronized void addSubscription(Subscription s, PublicationListener<?> listener) {
        this.managedSubscriptions.put(s, listener);
    }

    /**
     * @return {@link Map}
     */
    public synchronized Map<Subscription, PublicationListener<?>> getManagedSubscriptions() {
        return new HashMap<Subscription, PublicationListener<?>>(this.managedSubscriptions);
    }

    /**
     * @return {@link Collection} de {@link Subscription}
     */
    public synchronized Collection<Subscription> getSubscriptions() {
        return new ArrayList<Subscription>(this.managedSubscriptions.keySet());
    }

    /**
     * Devolve uma copia simples deste {@link SubscriptionSet}
     * 
     * @return {@link SubscriptionSetRepresentation}
     */
    public SubscriptionSetRepresentation toRepresentation() {
        return new SubscriptionSetRepresentation(this.getToken(), this.getVersion(), this.nextTimeout());
    }

    /**
     * Remove a subscription
     * 
     * @param sub {@link Subscription}
     */
    public synchronized void removeSubscription(Subscription sub) {
        this.managedSubscriptions.remove(sub);
        this.remainingPublications.remove(sub);
        this.tokens.remove(sub.getSubscriptionToken());
    }

    /**
     * 
     */
    public synchronized void removeAll() {
        this.managedSubscriptions.clear();
        this.remainingPublications.clear();
        this.tokens.clear();
    }

    /**
     * @param source {@link Publisher}
     * @param settings {@link SubscriptionSettings}
     * @return {@link Subscription}
     */
    public Subscription findSubscription(Publisher<?> source, SubscriptionSettings settings) {
        Collection<Subscription> subscriptions = this.getSubscriptions();
        for (Subscription subscription : subscriptions) {
            if (subscription.getPublisher().equals(source) && subscription.getSettings().equals(settings)) {
                return subscription;
            }
        }
        return null;
    }

    /**
     * Devolve o callback associado ao {@link Subscription} passado
     * 
     * @param sub {@link Subscription}
     * @return {@link PublicationListener}
     */
    public synchronized PublicationListener<?> getCallback(Subscription sub) {
        return this.managedSubscriptions.get(sub);
    }

    /**
     * Devolve
     * 
     * @param subscriptionToken o token da subscription
     * @return {@link Publication}
     */
    public synchronized Publication getPublication(long subscriptionToken) {
        Subscription subscription = this.tokens.get(subscriptionToken);
        Publication p = this.remainingPublications.remove(subscription);
        return p;
    }

    /**
     * Cria um novo {@link SubscriptionImpl}
     * 
     * @param settings o {@link SubscriptionSettings}
     * @param publisher o {@link Publisher}
     * @return {@link SubscriptionImpl}
     */
    public synchronized SubscriptionImpl newSubscription(SubscriptionSettings settings, Publisher<?> publisher) {
        final SubscriptionImpl s = new SubscriptionImpl(this.toRepresentation(), settings, publisher, this.tokenGenerator++);
        this.tokens.put(s.getSubscriptionToken(), s);
        return s;
    }

    /**
     * Devolve a subscription dado um token
     * 
     * @param token long
     * @return {@link Subscription}
     */
    public synchronized Subscription getSubscription(long token) {
        return this.tokens.get(token);
    }

}
